{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "52fc164d-dc72-459f-876f-81076f33dc54",
   "metadata": {},
   "source": [
    "### 代码练习：PCA+faiss优化实现KNN辨别猫狗"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "50a51546-9298-4c51-939e-89ee015c7338",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.decomposition import PCA\n",
    "import numpy as np\n",
    "import pickle # <-- 这个一定要学会\n",
    "import cv2 as cv\n",
    "import matplotlib.pyplot as plt\n",
    "import time\n",
    "from os.path import exists\n",
    "from imutils import paths # 使用这个\n",
    "import os\n",
    "import faiss\n",
    "\n",
    "############################## 函数声明部分 ##############################\n",
    "# 显示进度条\n",
    "def progress_bar(s,i):\n",
    "    print(\"\\r\", end=\"\")  # 输出位置回到行首\n",
    "    print(s+\": {}%: \".format(i), \"▋\" * (i // 2), end=\"\") \n",
    "\n",
    "# 把图像转化成指定长度的特征向量\n",
    "def createImageFeatures(image, size=(32, 32)):\n",
    "    image = cv.resize(image, size)\n",
    "    pixel_list = image.flatten()\n",
    "    return pixel_list\n",
    "\n",
    "# 获取/创建包含有所有图像的X和y\n",
    "def createXY(folder):\n",
    "    if exists(\"X\") and exists(\"y\"):\n",
    "        with open(\"X\", 'rb') as f:\n",
    "            X = pickle.load(f)\n",
    "        with open(\"y\", 'rb') as f:\n",
    "            y = pickle.load(f)\n",
    "        return X,y\n",
    "\n",
    "    print(\"读取所有图像，生成X和y\")\n",
    "    image_paths = list(paths.list_images(folder)) #从folder中获得所有的图像文件列表\n",
    "\n",
    "    X = []\n",
    "    y = []\n",
    "    i=1\n",
    "    for image_path in image_paths:\n",
    "        progress_bar(\"读取图像\",i*100//len(image_paths))\n",
    "        image = cv.imread(image_path, 0)\n",
    "        label = image_path.split(os.path.sep)[-1].split(\".\")[0]\n",
    "        if label=='dog':\n",
    "            label=1\n",
    "        else:\n",
    "            label=0\n",
    "        pixels = createImageFeatures(image)\n",
    "        X.append(pixels)\n",
    "        y.append(label)\n",
    "        i=i+1\n",
    "        \n",
    "    print(\"\\nX.shape:\",np.shape(X))\n",
    "    with open(\"X\", 'wb') as f:\n",
    "        pickle.dump(X, f)\n",
    "\n",
    "    print(\"y.shape:\",np.shape(y))\n",
    "    with open(\"y\", 'wb') as f:\n",
    "        pickle.dump(y, f) \n",
    "\n",
    "    return X,y\n",
    "\n",
    "############################## 使用 faiss-Gpu 实现的 KNN  ##############################\n",
    "class FaissKNeighbors:\n",
    "    def __init__(self, n_neighbors=5):\n",
    "        self.index = None\n",
    "        self.y = None\n",
    "        self.k = n_neighbors\n",
    "\n",
    "    def fit(self, X, y,res):\n",
    "        self.index = faiss.IndexFlatL2(X.shape[1])\n",
    "        self.index.add(X.astype(np.float32))\n",
    "    \n",
    "        #把 CPU index 转化成 GPU index, 0 表示要使用第0个GPU\n",
    "        self.index = faiss.index_cpu_to_gpu(res, 0, self.index) \n",
    "        self.y = y\n",
    "\n",
    "    def predict(self, X):\n",
    "        distances, indices = self.index.search(X.astype(np.float32), k=self.k)\n",
    "        votes = self.y[indices]\n",
    "        predictions = np.array([np.argmax(np.bincount(x)) for x in votes])\n",
    "        return predictions\n",
    "\n",
    "    def score(self, X, y):\n",
    "        return np.mean(self.predict(X)==y)\n",
    "\n",
    "############################## 程序逻辑部分 ##############################\n",
    "X,y = createXY(\".\")\n",
    "\n",
    "X = np.array(X)\n",
    "y = np.array(y) # 因为我的测试数据里的y是cat和dog的字符串，所以需要0和1\n",
    "print(\"成功读取X和y\")\n",
    "print(\"X的形状:\",X.shape)\n",
    "print(\"y的形状:\",y.shape)\n",
    "\n",
    "X = PCA(0.9).fit_transform(X)  # <--- 保留90%的变异性对应的维度\n",
    "print('PCA之后：x.shape:', X.shape)\n",
    "\n",
    "# 2. 分割X和y\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)\n",
    "print(y_test)\n",
    "with open(\"X_train\", 'wb') as f:\n",
    "    pickle.dump(X_train,f)\n",
    "    print(\"已生成：X_train, 形状:\",np.shape(X_train))\n",
    "    \n",
    "ngpus = faiss.get_num_gpus()\n",
    "print(\"number of GPUs:\", ngpus)\n",
    "# 获取GPU资源\n",
    "res = faiss.StandardGpuResources()\n",
    "\n",
    "# 3. 搜索最佳K\n",
    "print(\"开始搜索最佳K:\")\n",
    "start = time.time() # <--- 开始计时\n",
    "acc_list=[]\n",
    "max_test=300\n",
    "for k in range(1,max_test+1):\n",
    "    clf = FaissKNeighbors(n_neighbors=k)      # 1. 创建分类器\n",
    "    clf.fit(X_train, y_train, res)                 # 2. fit\n",
    "    acc = clf.score(X_test, y_test)           # 3. score\n",
    "    acc_list.append(acc) # 纪律所有的准确率\n",
    "end = time.time() # <--- 结束计时\n",
    "\n",
    "# 保存最大准确率对应的knn\n",
    "max_acc_k=np.argmax(acc_list)+1 # 利用最大准确率的位置得到对应的k：max_acc_k\n",
    "# 训练出 max_acc_k 对应的 knn 模型\n",
    "clf =  FaissKNeighbors(n_neighbors=max_acc_k).fit(X_train, y_train,res)\n",
    "with open(\"knn\", 'wb') as f: \n",
    "    pickle.dump(clf, f)\n",
    "    print(\"已保存具有最大准确率knn\")\n",
    "    \n",
    "# 保存测试结果到pdf\n",
    "plt.plot(range(1,max_test+1),acc_list)\n",
    "plt.title(\"duration: {0:.2f} Sec, max accuracy: {1:.2f}% (k={2})\".format((end - start),max(acc_list) * 100,max_acc_k))\n",
    "plt.savefig(\"acc_{0}_PAC_Faiss_GPU.pdf\".format(max_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4b0082ed-8e46-4bcf-a374-5f567c5af321",
   "metadata": {},
   "source": [
    "#### 参考答案"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "66ecc788-b6c2-424e-b971-d03ba0824308",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "成功读取X和y\n",
      "X的形状: (25000, 50)\n",
      "y的形状: (25000,)\n",
      "已生成：X_train, 形状: (18750, 50)\n",
      "开始搜索最佳K:\n",
      "已保存具有最大准确率knn\n"
     ]
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.model_selection import train_test_split\n",
    "import numpy as np\n",
    "import pickle # <-- 这个一定要学会\n",
    "import cv2 as cv\n",
    "import matplotlib.pyplot as plt\n",
    "import time\n",
    "from os.path import exists\n",
    "from imutils import paths # 使用这个\n",
    "import os\n",
    "import faiss\n",
    "from sklearn.decomposition import PCA\n",
    "from skimage.feature import hog\n",
    "\n",
    "############################## 函数声明部分 ##############################\n",
    "# 显示进度条\n",
    "def progress_bar(s,i):\n",
    "    print(\"\\r\", end=\"\")  # 输出位置回到行首\n",
    "    print(s+\": {}%: \".format(i), \"▋\" * (i // 2), end=\"\") \n",
    "\n",
    "\n",
    "# 获取/创建包含有所有图像的X和y\n",
    "def createXY(folder):\n",
    "    if exists(\"X\") and exists(\"y\"):\n",
    "        with open(\"X\", 'rb') as f:\n",
    "            X = pickle.load(f)\n",
    "        with open(\"y\", 'rb') as f:\n",
    "            y = pickle.load(f)\n",
    "        return X,y\n",
    "\n",
    "    print(\"读取所有图像，生成X和y\")\n",
    "    image_paths = list(paths.list_images(folder)) #从folder中获得所有的图像文件列表\n",
    "\n",
    "    X = []\n",
    "    y = []\n",
    "    i=1\n",
    "    for image_path in image_paths:\n",
    "        progress_bar(\"读取图像\",i*100//len(image_paths))\n",
    "        image = cv.imread(image_path, 0)\n",
    "        label = image_path.split(os.path.sep)[-1].split(\".\")[0]\n",
    "        if label=='dog':\n",
    "            label=1\n",
    "        else:\n",
    "            label=0\n",
    "        pixels = createImageFeatures(image)\n",
    "        X.append(pixels)\n",
    "        y.append(label)\n",
    "        i=i+1\n",
    "        \n",
    "    print(\"\\nX.shape:\",np.shape(X))\n",
    "    with open(\"X\", 'wb') as f:\n",
    "        pickle.dump(X, f)\n",
    "\n",
    "    print(\"y.shape:\",np.shape(y))\n",
    "    with open(\"y\", 'wb') as f:\n",
    "        pickle.dump(y, f) \n",
    "\n",
    "    return X,y\n",
    "\n",
    "############################## 使用 faiss-cpu 实现的 KNN  ##############################\n",
    "class FaissKNeighbors:\n",
    "  def __init__(self, n_neighbors=5):\n",
    "    self.index = None\n",
    "    self.y = None\n",
    "    self.k = n_neighbors\n",
    "    \n",
    "  # CPU版本\n",
    "  def fit(self, X, y):\n",
    "    # 把所有X中的数据放到IndexFlatL2索引对象中\n",
    "    self.index = faiss.IndexFlatL2(X.shape[1])\n",
    "    self.index.add(X.astype(np.float32))\n",
    "    self.y = y\n",
    "  ############# GPU版本 #############\n",
    "  # def fit(self, X, y,res): # <-- 需要额外接收一个GPU资源\n",
    "  #   self.index = faiss.IndexFlatL2(X.shape[1])\n",
    "  #   self.index.add(X.astype(np.float32))\n",
    "  #   self.index = faiss.index_cpu_to_gpu(res, 0, self.index) # <-- CPU index -> GPU index\n",
    "  #   self.y = y\n",
    "  ###################################\n",
    "  \n",
    "  def predict(self, X):\n",
    "    # 返回待测值的k个最近邻的距离排序矩阵和索引值排序矩阵\n",
    "    distances, indices = self.index.search(X.astype(np.float32), k=self.k)\n",
    "    votes = self.y[indices] # 获得所有最近邻的类别列表votes\n",
    "    # 得到votes中出现次数最多的类别，即为X的类别\n",
    "    predictions = np.array([np.argmax(np.bincount(x)) for x in votes])\n",
    "    return predictions\n",
    "\n",
    "  def score(self, X, y):\n",
    "    return np.mean(self.predict(X)==y)\n",
    "\n",
    "############################## 程序逻辑部分 ##############################\n",
    "X,y = createXY(\".\")\n",
    "X = np.array(X) # <-- faiss 需要用numpy\n",
    "X = PCA(0.9).fit_transform(X) # <-- 在读取后的X上PCA\n",
    "y = np.array(y)\n",
    "print(\"成功读取X和y\")\n",
    "print(\"X的形状:\",X.shape)\n",
    "print(\"y的形状:\",y.shape)\n",
    "\n",
    "# 2. 分割X和y\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)\n",
    "with open(\"X_train\", 'wb') as f:\n",
    "    pickle.dump(X_train,f)\n",
    "    print(\"已生成：X_train, 形状:\",np.shape(X_train))\n",
    "\n",
    "# 3. 搜索最佳K\n",
    "print(\"开始搜索最佳K:\")\n",
    "start = time.time() # <--- 开始计时\n",
    "acc_list=[]\n",
    "max_test=300\n",
    "##############\n",
    "# res = faiss.StandardGpuResources() # <-- 创建一个GPU资源\n",
    "##############\n",
    "for k in range(1,max_test+1):\n",
    "    clf = FaissKNeighbors(n_neighbors=k) # 1. 创建分类器\n",
    "    clf.fit(X_train, y_train)                 # 2. fit\n",
    "    #########################\n",
    "    # clf.fit(train_X, train_y,res) <--- 2. GPU fit\n",
    "    #########################\n",
    "    acc = clf.score(X_test, y_test)           # 3. score\n",
    "    # print(\"K={0} 准确率: {1:.2f}%\".format(k, acc * 100))\n",
    "    acc_list.append(acc) # 纪律所有的准确率\n",
    "end = time.time() # <--- 结束计时\n",
    "\n",
    "# 保存最大准确率对应的knn\n",
    "max_acc_k=np.argmax(acc_list)+1 # 利用最大准确率的位置得到对应的k：max_acc_k\n",
    "# 训练出 max_acc_k 对应的 knn 模型\n",
    "clf = FaissKNeighbors(n_neighbors=max_acc_k).fit(X_train, y_train)\n",
    "with open(\"knn\", 'wb') as f: \n",
    "    pickle.dump(clf, f)\n",
    "    print(\"已保存具有最大准确率knn\")\n",
    "\n",
    "# 保存测试结果到pdf\n",
    "plt.plot(range(1,max_test+1),acc_list)\n",
    "plt.title(\"duration: {0:.2f} minutes, max accuracy: {1:.2f}% (k={2})\".format((end - start)/60,max(acc_list) * 100,max_acc_k))\n",
    "plt.savefig(\"acc_{0}.pdf\".format(max_test))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
